// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.client.clc;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.microsoft.tfs.client.clc.exceptions.InvalidOptionValueException;
import com.microsoft.tfs.client.clc.options.Option;
import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.Platform;

public abstract class OptionsMap {
    /*
     * Maps a name string to a Class instance, so we can look up options by
     * name. The key is a String, and the value is a Class.
     */
    private final Map _aliasesToOptions = new HashMap();

    /**
     * Maps an option to its canonical name, so we can print a nice list out for
     * help text with the correct names.
     */
    private final Map _optionsToCanonicalNames = new HashMap();

    /**
     * Returns the supported option prefixes for this platform. Unix only
     * supports POSIX style options (-) and Microsoft platforms support both
     * POSIX (-) and Microsoft (/) styles.
     *
     * This method will always return at least one character in the array.
     *
     * Call {@link OptionsMap#getPreferredOptionPrefix()} to get the preferred
     * option prefix for this platform.
     */
    public static final char[] getSupportedOptionPrefixes() {
        if (Platform.isCurrentPlatform(Platform.WINDOWS)) {
            return new char[] {
                '/',
                '-'
            };
        } else {
            return new char[] {
                '-'
            };
        }
    }

    /**
     * Returns the option prefix character that is preferred for this platform.
     * The preferred character should be used in all documentation generated by
     * this client for a given platform.
     *
     *
     *
     * @return the option prefix character to advertise on this platform.
     */
    public static final char getPreferredOptionPrefix() {
        return getSupportedOptionPrefixes()[0];
    }

    /**
     * Add an option in the hash map.
     *
     * @param optionClass
     *        the option class.
     * @param names
     *        all names the option can be known by.
     */
    protected void putOption(final Class optionClass, final String[] names) {
        Check.isTrue(names.length > 0, "names.length > 0"); //$NON-NLS-1$

        _optionsToCanonicalNames.put(optionClass, names[0]);

        for (int i = 0; i < names.length; i++) {
            _aliasesToOptions.put(names[i], optionClass);
        }
    }

    /**
     * Creates an instance of the option class that matches the supplied option
     * name.
     *
     * @param optionNameWithValues
     *        the name (or partial name) of the option to create, including any
     *        option values.
     * @return a new Option instance that matches the given option name, null if
     *         no matches found.
     * @throws InvalidOptionValueException
     *         if the option values (the text following a colon) were not
     *         correct for this type of option.
     */
    public Option findOption(final String optionNameWithValues) throws InvalidOptionValueException {
        // An option must be at least two characters (including the switch).
        if (optionNameWithValues.length() < 2) {
            return null;
        }

        /*
         * The optionName will be a string like "/option", "/option:name",
         * "/option:username,p455,w0rd" (where password can include commas), or
         * "/option:val1,val2,val3". First, split up the string into its parts.
         */

        final int colonIndex = optionNameWithValues.indexOf(':');

        /*
         * The option can't end with a colon.
         */
        if (colonIndex == optionNameWithValues.length() - 1) {
            final String messageFormat = Messages.getString("OptionsMap.OptionValueMustBeSpecifiedFormat"); //$NON-NLS-1$
            final String message = MessageFormat.format(messageFormat, optionNameWithValues.substring(1, colonIndex));
            throw new InvalidOptionValueException(message);
        }

        final String optionName =
            optionNameWithValues.substring(1, (colonIndex != -1) ? colonIndex : optionNameWithValues.length());

        /*
         * If we found a colon, there will be arguments, so put those in a
         * string for later. Once we instantiate the object, we'll feed it these
         * values.
         */
        final String optionValueString = (colonIndex != -1) ? optionNameWithValues.substring(colonIndex + 1) : null;

        /*
         * Try to instantiate the correct Option instance.
         */
        Option o = null;
        for (final Iterator i = _aliasesToOptions.keySet().iterator(); i.hasNext();) {
            final Object key = i.next();

            final String alias = (String) key;

            // TODO Support partial matches.
            if (alias.compareToIgnoreCase(optionName) == 0) {
                o = instantiateOption(alias);

                o.setMatchedAlias(alias);
                o.setUserText(optionName);

                break;
            }
        }

        if (o == null) {
            return null;
        }

        /*
         * Here is where we finish setting the option values on the object.
         */
        o.parseValues(optionValueString);

        return o;
    }

    public Option instantiateOption(final String optionName) {
        // We found it. Create an instance.
        try {
            return (Option) ((Class) _aliasesToOptions.get(optionName)).newInstance();
        } catch (final Exception e) {
            return null;
        }
    }

    /**
     * Gets the hashmap used to map options to canonical names.
     *
     * @return the HashMap keyed on option: the value is the option's canonical
     *         name.
     */
    public Map getOptionsToCanonicalNamesMap() {
        return _optionsToCanonicalNames;
    }
}
